ECharts(Enterprise Charts)是一个基于JavaScript的开源数据可视化图表库,最初由百度开发,现由Apache软件基金会维护,它功能强大、交互性强,可用于Web页面上的数据可视化,实现包括折线图、柱状图、饼图、散点图、地图等交互图表的制作。

在R语言中,有很多包可以制作ECharts,其中echarty使用原生API,命令简洁,可实现ECharts的大部分功能。

使用echarty绘图时,对于基础图表,如折线图、柱状图等,可参考ECharts案例,将其中的JS代码改写为R代码即可,下面是一个简要的例子。

echarty折线图

参考ECharts案例:https://echarts.apache.org/examples/zh/editor.html?c=line-stack

# 加载包
library(echarty) 

# 画图
ec.init(
  title = list(text = "Stacked Line"),
  tooltip = list(trigger = "axis"),
  legend = list(data = c("Email", "Union Ads", "Video Ads", "Direct", "Search Engine")),
  grid = list(left = "3%", right = "4%", bottom = "3%", containLabel = TRUE),
  toolbox = list(
    feature = list(
      saveAsImage = list()
    )
  ),
  xAxis = list(
    type = "category",
    boundaryGap = FALSE,
    data = c("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
  ),
  yAxis = list(type = "value"),
  series = list(
    list(
      name = "Email",
      type = "line",
      stack = "Total",
      data = c(120, 132, 101, 134, 90, 230, 210)
    ),
    list(
      name = "Union Ads",
      type = "line",
      stack = "Total",
      data = c(220, 182, 191, 234, 290, 330, 310)
    ),
    list(
      name = "Video Ads",
      type = "line",
      stack = "Total",
      data = c(150, 232, 201, 154, 190, 330, 410)
    ),
    list(
      name = "Direct",
      type = "line",
      stack = "Total",
      data = c(320, 332, 301, 334, 390, 330, 320)
    ),
    list(
      name = "Search Engine",
      type = "line",
      stack = "Total",
      data = c(820, 932, 901, 934, 1290, 1330, 1320)
    )
  )
)

对比可以发现,使用echarty绘图,只要把ECharts的JS改写成R中的list就行了,非常简单:

然而,当绘制地图时,情况就有些复杂了。多次尝试后,我总结了以下内容,可以便捷地用echarty绘制地图。

需要注意的:

  1. 这里按地图类别自定义了几个绘图函数(注意,函数参数仅为方便本次画图,实际使用时,更多细节需在函数内部调整。函数参数maplevel并不严格,仅是非中国省级地图时,值不能为china,否则echarty包会在地图上额外添加一个南海诸岛图):

    • 针对数值数据的填充图:mapplot_fill_value
    • 针对分类数据的填充图:mapplot_fill_class
    • 针对数值数据的散点图:mapplot_point_value
    • 针对分类数据的散点图:mapplot_point_class
  2. echarty绘制地图需要geojson/json地图文件,两者只要是内部没错误,扩展名可以直接更改。

  3. 在绘制地图即绘图函数series中的type = "map"时,可使用ec.data函数将data.frame数据转list数据,此时需要指定format参数为names,并且绘图数据的列名必须为namevalue

  4. 绘图数据中的name列,其值决于geojson中的name的值,读取geojson文件后可通过sapply(map$features, function(feature) feature$properties$name)查看。

  5. 如需修改地图上name标签的显示位置,可以在geojson文件中,name后添加如下属性以自定义显示位置:“cp”:[114.505,22.050]。

  6. 必需要注册地图,然后它才能显示出来。

  7. 如果geojson文件过大,渲染地图会非常耗时,此时如果对精度要求不高,可以使用下面的mapshaper简化地图。

  8. 文中所用的数据,见github。注意中国-省级地图以及中国-市级地图都已经过多边形简化,并不精确。

geojson地图文件的相关连接

  1. 在线工具mapshaper,可用于地图精简、编辑:https://mapshaper.org/

  2. 在线工具Mapbox geojson,可进行地图编辑:https://geojson.io/

  3. 开源地信软件QGis,可用于geojson缩放,下载地址:https://download.osgeo.org/qgis/win64/

  4. geojson地图资源:

注意:GeoJson下载的topojson地图,可以用mapshaper将其转换为geojson/json使用

以下是地图绘制函数的定义,以及三个绘图示例

定义四个绘制地图函数

# 加载包
library(echarty)
library(magrittr)

# 函数1,针对数值数据的填充图 ##################################################################
mapplot_fill_value <- function(df_value, jsonmap, 
                               maplevel = "china",
                               showlabel = T,
                               showcolorbar = T){
  # 画地图函数
  ecmap <- ec.init(
    # 数据序列
    series = list(
      list(
        type = "map",
        map = maplevel,
        label = list(
          show = showlabel,
          formatter = htmlwidgets::JS("
          function(params) {
            if (params.name == '南海诸岛' || params.name == '十段线') {
              return '';  // 返回空字符串,隐藏这些标签
            }
            return params.name;
          }
        ")
        ),
        data = ec.data(df_value, format = "names")
      )
    ),
    # 鼠标悬停提示标签样式
    tooltip = list(
      trigger = "item",
      formatter = htmlwidgets::JS("
      function(params) {
        return params.name + ': ' + params.value + '';
      }
    ")
    ),
    # 工具条(数据、还原、下载)
    toolbox = list(
      show = TRUE,
      orient = "vertical",  #垂直
      left = "left",        #位置在左侧
      top = "45%",          #距离顶部的距离
      feature = list(
        dataView = list(readOnly = FALSE),
        restore = list(),
        saveAsImage = list()
      )
    ),
    # 图例(colorbar)
    visualMap = list(
      show = showcolorbar,
      min = min(df_value$value),
      max = max(df_value$value),
      # inRange = list(color = c("#ECF9FF", "#5470C6")),  #调整颜色范围
      inRange = list(color = c("lightskyblue", "yellow", "orangered")),
      orient = "vertical",   #垂直显示
      right = "100px",       #设置右侧的偏移
      top = "45%",           #设置距离顶部的偏移
      itemWidth = 20,        #设置每个项的宽度
      itemHeight = 250,      #设置每个项的高度
      calculable = TRUE      #可计算范围
    )
  )
  
  # 注册并返回地图
  ecmap$x$registerMap <- list(list(mapName=maplevel, geoJSON=jsonmap))
  return(ecmap)
}

# 函数2,针对分类数据的填充图 ##################################################################
mapplot_fill_class <- function(df_class, jsonmap, 
                               maplevel = "china",
                               showlabel = TRUE, 
                               showlegend = TRUE,
                               class_order = NULL) {
  # 使用用户自定义顺序,或默认顺序,注意默认顺序legend显示由低到高,所以需要rev()
  if (is.null(class_order)) {
    class_names <- unique(df_class$class) %>% rev()
  } else {
    class_names <- class_order %>% rev()
  }
  
  # 将分类转为数值ID(根据顺序)
  df_class$cat_id <- match(df_class$class, class_names)
  
  # 设置颜色
  # colors <- rainbow(6)   #特定数据绘图,可以在此自定义颜色
  colors <- scales::hue_pal()(length(class_names))
  
  # 给定显示颜色
  visual_pieces <- lapply(seq_along(class_names), function(i) {
    list(
      value = i,
      label = class_names[i],
      color = colors[i]
    )
  })
  
  # 地图数据
  data_map <- df_class %>% dplyr::transmute(name = name, value = cat_id, class = class)
  
  # 绘图对象
  ecmap <- ec.init(
    # 数据序列
    series = list(
      list(
        # name = "地区分类",
        type = "map",
        map = maplevel,
        roam = TRUE,
        roam = TRUE,                     #支持缩放和平移
        scaleLimit = list(
          min = 0.5,                     #最小缩放比例
          max = 2                        #最大缩放比例
        ),
        label = list(
          show = showlabel,
          formatter = htmlwidgets::JS("
          function(params) {
            if (params.name == '南海诸岛' || params.name == '十段线') {
              return '';
            }
            return params.name;
          }
        ")
        ),
        data = ec.data(data_map, format = "names")
      )
    ),
    # 鼠标悬停
    tooltip = list(
      trigger = "item",
      formatter = htmlwidgets::JS("
        function(params) {
          return params.name + '(' + params.data.class + ')';
        }
      ")
    ),
    # 图例
    visualMap = list(
      type = "piecewise",
      show = showlegend,
      pieces = visual_pieces,
      orient = "vertical",
      left = "right",
      top = "center",
      itemWidth = 20,
      itemHeight = 20
    ),
    # 工具条
    toolbox = list(
      show = TRUE,
      orient = "vertical",  #垂直
      left = "left",        #位置在左侧
      top = "45%",          #距离顶部的距离
      feature = list(
        saveAsImage = list(),
        restore = list()
      )
    )
  )
  # 注册并返回地图
  ecmap$x$registerMap <- list(list(mapName = maplevel, geoJSON = jsonmap))
  return(ecmap)
}

# 函数3,针对数值数据的散点图 ##################################################################
mapplot_point_value <- function(df_value, jsonmap,
                                maplevel = "china", 
                                showcolorbar = TRUE,
                                dpointsize = TRUE){
  # 散点数据结构:[lon, lat, value]
  scatter_data <- lapply(seq_len(nrow(df_value)), function(i) {
    list(
      name = df_value$name[i],
      value = c(df_value$lon[i], df_value$lat[i], df_value$value[i])
    )
  })
  
  # 散点动态大小的js代码
  minVal <- min(df_value$value)
  maxVal <- max(df_value$value)
  if(dpointsize){
    symbolSizeJS <- sprintf(
      "function(val){
      var minVal = %f;
      var maxVal = %f;
      var minSize = 8;                //最小点大小
      var maxSize = 18;               //最大点大小
      var size = minSize + (val[2] - minVal) / (maxVal - minVal) * (maxSize - minSize);
      return size;
      }",
      minVal, maxVal
    )
  } else{
    symbolSizeJS <- "function(val){ return 8; }"
  }
  
  # 画图
  ecmap <- ec.init(
    # 地图背景
    geo = list(
      map = maplevel,
      roam = TRUE,                     #支持缩放和平移
      scaleLimit = list(
        min = 0.5,                     #最小缩放比例
        max = 2                        #最大缩放比例
      ),
      itemStyle = list(
        areaColor = "#f5f5f5",
        borderColor = "#999"
      ),
      emphasis = list(               #禁止悬停强调
        itemStyle = list(
          areaColor = "#f5f5f5",     #鼠标悬停时区域颜色不变
          borderColor = "#999"       #边界颜色不变
        ),
        label = list(
          show = FALSE               #禁止显示悬停label
        )
      )
      # tooltip = list(show = FALSE)   #禁止悬停效果,全局的,禁后散点的悬停也没有了
    ),
    # 数据序列
    series = list(
      list(
        type = "scatter",
        coordinateSystem = "geo",
        data = scatter_data,
        symbolSize = htmlwidgets::JS(symbolSizeJS),       #散点大小
        label = list(
          show = TRUE,
          position = "right",
          formatter = "{b}"  # 显示城市名称
        ),
        itemStyle = list(
          opacity = 0.9,
          borderColor = "black",
          borderWidth = 1
        )
      )
    ),
    # 鼠标悬停效果
    tooltip = list(
      trigger = "item",
      formatter = htmlwidgets::JS("
      function(params) {
        return params.name + ': ' + params.value[2];
      }
    ")
    ),
    # 图例
    visualMap = list(
      show = showcolorbar,
      min = min(df_value$value),
      max = max(df_value$value),
      calculable = TRUE,
      inRange = list(color = c("lightskyblue", "yellow", "orangered")),
      orient = "vertical",
      right = "20px",
      top = "center"
    ),
    # 工具条
    toolbox = list(
      show = TRUE,
      orient = "vertical",  #垂直
      left = "left",        #位置在左侧
      top = "45%",          #距离顶部的距离
      feature = list(
        dataView = list(readOnly = FALSE),
        restore = list(),
        saveAsImage = list()
      )
    )
  )
  
  # 注册并返回地图
  ecmap$x$registerMap <- list(list(mapName = maplevel, geoJSON = jsonmap))
  return(ecmap)
}

# 函数4,针对分类数据的散点图 ##################################################################
mapplot_point_class <- function(df_class, jsonmap, 
                                maplevel = "china", 
                                showlabel = TRUE, 
                                class_order = NULL) {
  # 设定分类顺序
  if (is.null(class_order)) {
    class_names <- unique(df_class$class)
  } else {
    class_names <- class_order
  }
  
  # 设置颜色
  # colors <- rainbow(6)   #特定数据绘图,可以在此自定义颜色
  colors <- scales::hue_pal()(length(class_names))
  
  # 每类一个 scatter series
  series_list <- lapply(seq_along(class_names), function(i) {
    cname <- class_names[i]
    cdata <- df_class[df_class$class == cname, ]
    scatter_data <- lapply(seq_len(nrow(cdata)), function(j) {
      list(
        name = cdata$name[j],
        value = c(cdata$lon[j], cdata$lat[j], cname),
        class = cname
      )
    })
    list(
      name = cname,                 #影响legend的label显示顺序
      type = "scatter",
      coordinateSystem = "geo",
      data = scatter_data,
      symbolSize = 12,
      label = list(
        show = showlabel,
        position = "right",
        formatter = "{b}"
      ),
      itemStyle = list(
        color = colors[i],
        borderColor = "black",
        borderWidth = 0.5,
        opacity = 0.9
      )
    )
  })
  
  # 绘图
  ecmap <- ec.init(
    # 地图背景
    geo = list(
      map = maplevel,
      roam = TRUE,                     #支持缩放和平移
      scaleLimit = list(
        min = 0.5,                     #最小缩放比例
        max = 2                        #最大缩放比例
      ),
      itemStyle = list(
        areaColor = "#f5f5f5",
        borderColor = "#999"
      ),
      emphasis = list(
        itemStyle = list(
          areaColor = "#f5f5f5",
          borderColor = "#999"
        ),
        label = list(show = FALSE)
      )
    ),
    # 数据序列
    series = series_list,
    # 图例
    legend = list(
      type = "scroll",
      orient = "vertical",
      left = "right",
      top = "center",
      data = class_names,      #明确指定legend顺序
      itemWidth = 12,
      itemHeight = 12,
      itemStyle = list(
        borderColor = "black",
        borderWidth = 0.5,
        opacity = 0.9
      )
    ),
    # 鼠标悬停
    tooltip = list(
      trigger = "item",
      formatter = htmlwidgets::JS("
        function(params) {
          return params.name + '(' + params.data.class + ')';
        }
      ")
    ),
    # 工具条
    toolbox = list(
      show = TRUE,
      orient = "vertical",  #垂直
      left = "left",        #位置在左侧
      top = "45%",          #距离顶部的距离
      feature = list(
        saveAsImage = list(),
        restore = list()
      )
    )
  )
  # 注册并返回地图
  ecmap$x$registerMap <- list(list(mapName = maplevel, geoJSON = jsonmap))
  return(ecmap)
}

绘图案例

1.1 数值填充型,世界-国级地图

# 加载包
library(echarty)
library(jsonlite) 
library(magrittr) 

# 读取地图
map_world_county <- read_json("world_country.json")

# 识别name标签
country_name <- sapply(map_world_county$features, function(feature) feature$properties$name) %>%
  Filter(nzchar, .)

# 随机数据
df_world <- data.frame(name = country_name,
                       value = runif(length(country_name), 0, 100) %>% round(2))

# 执行画图
mapplot_fill_value(df_world, map_world_county, 
                   maplevel = "world", 
                   showlabel = F, 
                   showcolorbar = F)

1.2 数值填充型,中国-省级地图

# 加载包
library(echarty)
library(jsonlite) 

# 示例数据
df_china <- data.frame(name = c('安徽', '澳门', '北京', '福建', '甘肃', '广东', 
                                '广西', '贵州', '海南', '河北', '河南', '黑龙江', 
                                '湖北', '湖南', '吉林', '江苏', '江西', '辽宁', 
                                '内蒙古', '宁夏', '青海', '山东', '山西', '陕西', 
                                '上海', '四川', '台湾', '天津', '西藏', '香港', 
                                '新疆', '云南', '浙江', '重庆'),  
                       value = c(2727, 159, 6397, 2819, 1257, 3332, 1548, 1557, 
                                 124, 1923, 3641, 2896, 8236, 8640, 3153, 2255,
                                 3954, 4416, 421, 417, 603, 825, 1228, 1397, 3748,
                                 587, 62, 2289, 239, 361, 662, 5215, 108, 492))

# 中国省级地图
map_china_province <- read_json("china_province_simplified.json")

# 执行画图
mapplot_fill_value(df_china, map_china_province,
                   maplevel = "china")

2.1 分类填充型,中国-省级地图

分类显示中国经济地理区划

# 加载包
library(echarty)
library(jsonlite)
library(magrittr)
# library(dplyr)

# 示例分类数据(中国自然资源部版划分法)
df_class <- data.frame(
  name = c('北京', '天津', '河北', '山东', '山西', '内蒙古',       #华北6个
           '辽宁', '吉林', '黑龙江',                               #东北3个
           '上海', '江苏', '浙江', '安徽',                         #华东4个
           '广东', '广西', '福建', '海南',                         #华南4个 
           '湖北', '湖南', '河南', '江西',                         #华中4个 
           '重庆', '四川', '贵州', '云南', '西藏',                 #西南5个
           '陕西', '甘肃', '青海', '宁夏', '新疆',                 #西北5个
           '香港', '澳门', '台湾'),                                #港澳台3个
  class = c('华北', '华北', '华北', '华北', '华北', '华北', 
            '东北', '东北', '东北',
            '华东', '华东', '华东', '华东', 
            '华南', '华南', '华南', '华南',
            '华中', '华中', '华中', '华中',
            '西南', '西南', '西南', '西南', '西南',
            '西北', '西北', '西北', '西北', '西北',
            '港澳台', '港澳台', '港澳台')
)
my_order <- c("华北", "东北","华中","华东", "华南", "西北", "西南", "港澳台") 

# 中国省级地图
map_china_province <- read_json("china_province_simplified.json")

# 执行画图
mapplot_fill_class(df_class, map_china_province, 
                   showlabel = F, 
                   showlegend = T, 
                   class_order = my_order)

2.2 分类填充型,中国-市级地图

分类显示中国地市级经济发展水平,其中,城市分级来自《2025新一线城市魅力排行榜》,而地图数据中部分城市区划变更未更新,与排行榜不吻合,其中,所有在排行榜中的城市,均已标记在地图上,但地图上某些城市未出现在排行榜上。

# 加载包
library(echarty)
library(jsonlite)
library(magrittr)
# library(dplyr)

# 读取数据
dat <- read.csv("city_tier.csv")

# 中国市级地图
map_china_city <- read_json("china_city_simplified.json")

# 生成数据
dat_city_tier <- data.frame(name = dat$name_area, class = dat$type)
tierorder <- c("一线城市", "新一线城市", "二线城市", "三线城市", "四线城市", "五线城市")

# 执行画图
mapplot_fill_class(dat_city_tier, map_china_city,
                   maplevel = "others",               #这里如果是china则会有额外的南海诸岛图
                   showlabel = F, 
                   showlegend = T, 
                   class_order = tierorder)

3. 数值散点图,广东地级市

在地图上添加数据点,其中点展示数值型数据,地图仅作为背景。

# 加载包
library(echarty)
library(jsonlite)
library(magrittr)

# 读取城市坐标数据
data <- read.csv("city_tier.csv")

# 读取地图
map_gd_province <- read_json("guangdong_province.geojson")

# 生成随机绘图数据
dat_city_value <- data[data$province=="广东省",]
dat_city_value$value <- runif(nrow(dat_city_value),0,100) %>% round(2)

# 执行画图
mapplot_point_value(dat_city_value, map_gd_province, 
                    maplevel = "guangdong_province", 
                    showcolorbar = T, 
                    dpointsize = F)

4. 分类散点图,中国地级城市

在地图上添加数据点,其中点展示分类型数据,地图仅作为背景。此图同样展示了《2025新一线城市魅力排行榜》中的城市等级划分情况。

# 加载包
library(echarty)
library(jsonlite)
library(magrittr)
# library(dplyr)

# 读取城市发展数据
dat <- read.csv("city_tier.csv", encoding = "UTF-8")

# 中国省级地图
map_china_province <- read_json("china_province_simplified.json")

# 生成地图数据
dat_city_tier <- data.frame(name = dat$name, 
                            lon = dat$lon,
                            lat = dat$lat,
                            class = dat$type)
tierorder <- c("一线城市", "新一线城市", "二线城市", "三线城市", "四线城市", "五线城市")

# 执行画图
mapplot_point_class(dat_city_tier, map_china_province, 
                    maplevel = "china_province",
                    showlabel = F,
                    class_order = tierorder)